package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.product.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * ClassName: TestServiceImpl
 * Description:     通过redis实现分布式锁
 *
 * @Author linz
 * @Creat 2025/3/18 20:35
 * @Version 1.00
 */
@Service
public class TestServiceImpl implements TestService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Value("${server.port}")
    private String port;

    //@Override
    public synchronized void testLockV0() {
        //查询redis中的num值
        String value = redisTemplate.opsForValue().get("num");

        //没有return
        if(StringUtils.isBlank(value)){
            return;
        }
        //转成int
        int num = Integer.parseInt(value);
        //将redis中的num加一
        redisTemplate.opsForValue().set("num",String.valueOf(++num));
        System.out.println("当前线程："+Thread.currentThread().getName()+"，当前num："+num);
/*        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
    }

    /**
     * 测试redis分布式锁 第一版本
     */
    //@Override
    public  void testLockV1() {
        //相当于redis中的setnx 这种操作的特性是 如果key不存在，则设置key的值为value，如果key存在，则不做任何操作。
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", "lock");

        //判断是否持有锁
        if(flag){
            //业务逻辑
            String value = redisTemplate.opsForValue().get("num");

            //没有return
            if(StringUtils.isBlank(value)){ //判断字符串是否为空或者只有字符串
                return;
            }
            //转成int
            int num = Integer.parseInt(value);
            //将redis中的num加一
            redisTemplate.opsForValue().set("num",String.valueOf(++num));
            System.out.println("当前线程："+Thread.currentThread().getName()+"，当前num："+num);

            //释放锁
            redisTemplate.delete("lock");
        }else { //如果暂时没拿到锁，重试
            //注意要间隔重试，不能无限重试，因为会消耗大量cpu资源
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            testLock();
        }
    }

    /**
     * 测试redis分布式锁 第二版本
     *
     *
     *      思考：如果线程A拿到锁执行业务逻辑报异常了或者宕机了，锁一直不能释放，那么其他线程就永远拿不到锁，就会一直重试，就会消耗大量cpu资源。
     *      如何优化？ -> 1. 加过期时间，如果过期时间到了，那么其他线程就可以拿到锁，然后重试。
     *                   2. 将整个业务放在try catch finally 中，无论如何都释放锁。
     */
    //@Override
    public  void testLockV2() {
        //相当于redis中的setnx 这种操作的特性是 如果key不存在，则设置key的值为value，如果key存在，则不做任何操作。
        //设置过期时间，如果过期时间到了，那么其他线程就可以拿到锁，然后重试。 且设置过期时间与获取锁为原子操作
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", "lock", 10, TimeUnit.SECONDS);

        //判断是否持有锁
        if(flag){
            try {
                //业务逻辑
                String value = redisTemplate.opsForValue().get("num");

                if(port.equals("9205")){
                    int a = 1/0;
                }
                if(StringUtils.isBlank(value)){ //判断字符串是否为空或者只有字符串
                    return;
                }
                //转成int
                int num = Integer.parseInt(value);
                //将redis中的num加一
                redisTemplate.opsForValue().set("num",String.valueOf(++num));
                System.out.println("当前线程："+Thread.currentThread().getName()+"，当前num："+num);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //释放锁
                redisTemplate.delete("lock");
            }
        }else { //如果暂时没拿到锁，重试
            //注意要间隔重试，不能无限重试，因为会消耗大量cpu资源
            try {
                Thread.sleep(20);
                testLock();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    /**
     * 测试redis分布式锁 第三版本
     *
     *
     *      思考：如果业务逻辑执行的时间是12秒，10秒中后锁自动释放了，
     *          别的线程拿到锁以后，别的线程会和之前的线程同时操作业务，相当于没锁
     *      如何优化？ -> 释放锁之前判断是否是自己线程的锁
     *                  又有问题：判断操作和删除操作不符合原子性 一个get 一个delete
     *                  可能index1执行删除时，查到的lock值确实和uuid相等，在index1执行删除前，lock刚好过期了,被redis自动释放
     *                  然后index2迅速获取锁执行业务，此时index1执行删除操作，又会导致index2中的锁被删除，从而锁的效果又失效了
     *
 *                      解决方案：1.尽量延长过期时间
     *                           2. 使用lua脚本，保证原子性
     *
     */
    //@Override
    public  void testLockV3() {
        //相当于redis中的setnx 这种操作的特性是 如果key不存在，则设置key的值为value，如果key存在，则不做任何操作。
        //设置过期时间，如果过期时间到了，那么其他线程就可以拿到锁，然后重试。 且设置过期时间与获取锁为原子操作
        String uuid = UUID.randomUUID().toString();
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock",uuid, 10, TimeUnit.SECONDS);

        //判断是否持有锁
        if(flag){
            try {
                //业务逻辑
                String value = redisTemplate.opsForValue().get("num");

                if(StringUtils.isBlank(value)){ //判断字符串是否为空或者只有字符串
                    return;
                }
                //转成int
                int num = Integer.parseInt(value);
                //将redis中的num加一
                redisTemplate.opsForValue().set("num",String.valueOf(++num));
                System.out.println("当前线程："+Thread.currentThread().getName()+"，当前num："+num);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //释放锁之前判断是否是自己的锁
                if(uuid.equals(redisTemplate.opsForValue().get("lock"))){
                    redisTemplate.delete("lock");
                }
            }
        }else { //如果暂时没拿到锁，重试
            //注意要间隔重试，不能无限重试，因为会消耗大量cpu资源
            try {
                Thread.sleep(20);
                testLock();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    /**
     * 测试redis分布式锁 第四版本
     *
     *                      第三版本的解决方案
     *                      解决方案：1.尽量延长过期时间
     *                           2. 使用lua脚本，保证原子性
     *
     */
    @Override
    public  void testLock() {
        String uuid = UUID.randomUUID().toString();
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock",uuid, 15, TimeUnit.SECONDS);

        //判断是否持有锁
        if(flag){
            try {
                //业务逻辑
                String value = redisTemplate.opsForValue().get("num");

                if(StringUtils.isBlank(value)){ //判断字符串是否为空或者只有字符串
                    return;
                }
                //转成int
                int num = Integer.parseInt(value);
                //将redis中的num加一
                redisTemplate.opsForValue().set("num",String.valueOf(++num));
                System.out.println("当前线程："+Thread.currentThread().getName()+"，当前num："+num);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //锁的释放
                String script = """
                    if redis.call('get', KEYS[1]) == ARGV[1]
                        then return redis.call('del', KEYS[1])
                    else
                        return 0
                    end
                    """;
                Boolean lock = redisTemplate.execute(
                        new DefaultRedisScript<>(script, Boolean.class),
                        Arrays.asList("lock"),
                        uuid);
                System.out.println("result = " + lock);
            }
        }else { //如果暂时没拿到锁，重试
            //注意要间隔重试，不能无限重试，因为会消耗大量cpu资源
            try {
                Thread.sleep(20);
                testLock();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
